package weeny.pre.display;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;

import prefuse.Visualization;
import prefuse.controls.PanControl;
import prefuse.controls.ZoomControl;
import prefuse.controls.ZoomToFitControl;
import prefuse.data.tuple.TupleSet;
import prefuse.data.util.ColumnProjection;
import prefuse.data.util.NamedColumnProjection;
import prefuse.util.GraphicsLib;
import prefuse.util.display.DisplayLib;
import prefuse.visual.EdgeItem;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;
import weeny.face.WidgetTabbedPane;
import weeny.pre.control.MoreDragControl;
import weeny.pre.control.MoreDragControl.DragListener;
import weeny.pre.event.GroupListener;
import weeny.pre.face.FilterBox;
import weeny.pre.face.ExpressionBox;
import weeny.pre.face.PreJTable;
import weeny.pre.face.operation.ColumnValueOperation;
import weeny.pre.face.operation.GroupOperation;
import weeny.pre.face.operation.OpacityValueOperation;
import weeny.pre.face.operation.OperationBox;
import weeny.pre.face.treetable.PreGroupTreeTableUI;
import weeny.pre.face.treetable.PreGroupModel;
import weeny.pre.face.treetable.PreJTreeTable;
import weeny.pre.util.Pre;
import weeny.util.logs.Debug;

import static prefuse.visual.VisualItem.*;


/**
 * This display gives some basic ui components. It split the jframe into two
 * parts by using JSplitPane. And the right part contains controling components.
 * There are two default control tabs it has here are: {@value #IO_DISPLAY_TAB}
 * and {@value #ITEM_TAB}.
 * 
 * Call {@link #showIODisplayTab()} to show IO&Display, which is easy:
 * 1) zoom / pan / drag control
 *    a) drag the righ mouse button in the empty space to zoom
 *    b) drag the left mouse button in the empty space to pan
 *    c) drag the left mouse button on the item to drag
 *    d) click right mouse button in the empty space to zoom-to-fit
 * 2) save pdf/png buttons
 *    See {@link PdfDisplay#createSaveImageBox(String)} for more details
 * 3) high/low rendering quality switch
 *    Turn on/off ani-aliasing control, if you feel the interaction is slow,
 *    try to turn it off
 * 4) edge drag switch
 *    If it is off, you can only adjust edge by draging its end points. But
 *    when it is on, you can directly drag an edge, and its both ends will be
 *    moved accordingly.
 * 5) highlighted items drag switch
 *    There is a boolean field in every visual item. So if this is on, when you
 *    drag an item (as long as it is dragable), every highlighted items are
 *    also move accordingly.
 *    
 * Call {@link #showItemControlTabInTables()} to show all visual tables
 * contained in it:
 * 1) To use this function, it has to be {@link MoreVisualization}.
 * 2) This tab shows all visual tables the visualization holds. The tables are
 *    displayed as PreJTables, how they work please see {@link PreJTable}.
 * 3) Besides that, there is a filter at the top, you can fill an expression to 
 *    be parsed, and it filters all visual tuples. The expression must have a 
 *    boolean value as the result. More info? Please see {@link ExpressionBox}
 * 4) Below the filter is an operation box, which can let you do things, like
 *    setting values, adding items to or removing items from focus groups, etc. 
 *    More details can be found in {@link OperationBox}. 
 * 
 * Call {@link #showItemControlTabInTree()} to show all groups contained in it:
 * 0) It is a better version of {@link #showItemControlTabInTables()}, because
 *    you may have better group control over items, and you will know all the
 *    grouped tuples information.
 * 1) Everything else is quite like {@link #showItemControlTabInTables()}.
 * 
 * These two functions, generally you should call them once each, otherwise, no
 * idea what may happen:).
 * 
 * There is one more function useful here: addWidget(String, Component).
 * We can add any component to a specific named tab. Every time the component
 * is added, it takes the while line. Basically, it is a sequence of JPanel,
 * one containing the next one, and they all use borderlayout. Every added
 * component is added to the NORTH part of one of them.
 * 
 * @author weeny
 *
 */
public class BaseUIDisplay extends PdfDisplay{
	
	//the default two tab names
	/**
	 * The name of the tab which shows file IO and display control, e.g., zoom,
	 * pan, group control, high quality control, and edge dragable control.
	 */
	public static final String IO_DISPLAY_TAB = "IO & Display";
	/**
	 * The name of the tab which shows the content of all visual tables for you
	 * to filter and set values or manipulate 
	 */
	public static final String ITEM_TAB = "Items";
	
	/**
	 * The default maximum dimension for tables. You may have a lot of items in
	 * a single table, but we do not have enough space to show them all at once.
	 */
	public static final Dimension TABLE_DIM = new Dimension(290, 250);
	
	/**
	 * The default maximum dimension for trees. 
	 */
	public static final Dimension TREE_DIM = new Dimension(290, 500);
	
	//display controls
	protected ZoomControl           zoomControl = null;
	protected ZoomToFitControl zoomToFitControl = null;
	protected PanControl             panControl = null;
	protected MoreDragControl       dragControl = null;
	
	protected WidgetTabbedPane widgetTabs = new WidgetTabbedPane();
	protected JComponent viewPane = null;

	//add to MoreDragControl to handle the edge drag behaviors. By default, you
	//can only drag edge ends to adjust the position of a edge. But using this
	//istener, you can directly drag edge to translate it
	protected DragListener edgeDragListener = new DragListener(){
		@Override
		public void afterItemDragged(VisualItem item, MouseEvent e) {
			if (item instanceof EdgeItem){
				//so it is edge and "edge dragable" is on
				double dx = item.getX()-item.getStartX();
				double dy = item.getY()-item.getStartY();
				Pre.shift(((EdgeItem)item).getSourceItem(), dx, dy);
				Pre.shift(((EdgeItem)item).getTargetItem(), dx, dy);
			}
		}
	};
	//add to MoreDragContol to handle the highlight drag behaviors. By default,
	//every item has a boolean field called [_highlighted]. If you add this
	//listener, you can set the values to true in any items you want and move
	//them all as a group
	protected DragListener highlightedDragListener = new DragListener(){
		private Set<VisualItem> alreadyAdjusted = new HashSet<VisualItem>();
		@Override
		public void afterItemDragged(VisualItem item, MouseEvent e) {
			if (!alreadyAdjusted.isEmpty()) {
				alreadyAdjusted.clear();
			}
			double dx = item.getX()-item.getStartX();
			double dy = item.getY()-item.getStartY();
			if (dx == 0 && dy == 0) {
				//if the mouse over item is not moved, stop
				return;
			}
			Iterator<VisualItem> it = m_vis.items();
			while (it.hasNext()){
				VisualItem vi = it.next();
				if (vi == item) {
					continue;//skip the mouse over item
				}
				if (vi.isHighlighted() && !alreadyAdjusted.contains(vi)) {
					//if it should and has not been adjusted
					if (vi instanceof EdgeItem) {
						EdgeItem edge = (EdgeItem) vi;
						if(!alreadyAdjusted.contains(edge.getSourceItem())) {
							alreadyAdjusted.add(edge.getSourceItem());
							Pre.shift(edge.getSourceItem(), dx, dy);
						}
						if(!alreadyAdjusted.contains(edge.getTargetItem())) {
							alreadyAdjusted.add(edge.getTargetItem());
							Pre.shift(edge.getTargetItem(), dx, dy);
						}
					}
					else {
						Pre.shift(vi, dx, dy);
					}
					alreadyAdjusted.add(vi);
				}
			}
		}
	};
	
	public BaseUIDisplay(Visualization vis){
		super(vis);
		if (!(vis instanceof weeny.pre.display.MoreVisualization)) {
			Debug.prob("Not a MoreVisualization (may causes problems later)");
		}
		setHighQuality(true);
	}
	public BaseUIDisplay() {
		this(new MoreVisualization());
	}
	
	public void setAllControl(boolean on){
		setZoomControl(on);
		setZoomToFitControl(on);
		setPanControl(on);
		setDragControl(on);
	}
	public void setZoomControl(boolean on){
		if (zoomControl == null && on) {
			zoomControl = new ZoomControl();
			this.addControlListener(zoomControl);
		}
		if (zoomControl != null && !on){
			this.removeControlListener(zoomControl);
			zoomControl = null;
		}
	}
	public void setZoomToFitControl(boolean on){
		if (zoomToFitControl == null && on) {
			zoomToFitControl = new ZoomToFitControl();
			this.addControlListener(zoomToFitControl);
		}
		if (zoomToFitControl != null && !on){
			this.removeControlListener(zoomToFitControl);
			zoomToFitControl = null;
		}
	}
	public void setPanControl(boolean on){
		if (panControl == null && on) {
			panControl = new PanControl();
			this.addControlListener(panControl);
		}
		if (panControl != null && !on){
			this.removeControlListener(panControl);
			panControl = null;
		}
	}
	
	public boolean addDragListener(DragListener listener){
		if (dragListeners.contains(listener)) {
			return false;
		}
		dragListeners.add(listener);
		if (dragControl != null) {
			setDragControl(false);
			setDragControl(true);
		}
		return true;
	}
	public boolean removeDragListener(DragListener listener){
		if (!dragListeners.contains(listener)) {
			return false;
		}
		dragListeners.remove(listener);
		if (dragControl != null) {
			setDragControl(false);
			setDragControl(true);
		}
		return true;
	}
	
	private List<DragListener> dragListeners = new ArrayList<DragListener>();
	public void setDragControl(boolean on){
		if (dragControl == null && on) {
			dragControl = new MoreDragControl();
			for(DragListener lis : dragListeners) {
				dragControl.addDragListener(lis);
			}
			this.addControlListener(dragControl);
		}
		if (dragControl != null && !on){
			this.removeControlListener(dragControl);
			dragControl = null;
		}
	}

	/**
	 * Add a AWT component to the right side tab panel
	 * @param tabName the tab name the component is added to, 
	 *        create it if necessary
	 * @param component the component to be added
	 */
	public void addWidget(String tabName, Component component){
		widgetTabs.addWidget(tabName, component);
	}
	
	/**
	 * create the display control of the view, including:
	 * zoom zoom-to-fit, pan, and drag
	 * @return a new box component including those check boxes
	 */
	public Box createDisplayControlBox(){
		Box controlBox = new Box(BoxLayout.X_AXIS);
		controlBox.setBorder(new TitledBorder("Zoom / Pan / Drag"));

		final JCheckBox zoomCheck    = new JCheckBox("zoom");
		final JCheckBox zoomFitCheck = new JCheckBox("zoom to fit");
		final JCheckBox panCheck     = new JCheckBox("pan");
		final JCheckBox dragCheck    = new JCheckBox("drag");
		
		zoomCheck.setSelected(zoomControl != null);
		zoomFitCheck.setSelected(zoomToFitControl != null);
		panCheck.setSelected(panControl != null);
		dragCheck.setSelected(dragControl != null);
		
		ActionListener controlActionListener = new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				JCheckBox box = (JCheckBox) e.getSource();
				if (box == zoomCheck) {
					setZoomControl(box.isSelected());
				}
				else if (box == zoomFitCheck){
					setZoomToFitControl(box.isSelected());
				}
				else if (box == panCheck){
					setPanControl(box.isSelected());
				}
				else if (box == dragCheck){
					setDragControl(box.isSelected());
				}
			}
		};
		zoomCheck.addActionListener(controlActionListener);
		zoomFitCheck.addActionListener(controlActionListener);
		panCheck.addActionListener(controlActionListener);
		dragCheck.addActionListener(controlActionListener);
		
		zoomCheck.setToolTipText("Drag right mouse button in the empty space");
		zoomFitCheck.setToolTipText("Right click in the empty space");
		dragCheck.setToolTipText("Drag left mouse button on the visual item");
		panCheck.setToolTipText("Drag left mouse button in the empty space");

		controlBox.add(zoomCheck);
		controlBox.add(zoomFitCheck);
		controlBox.add(panCheck);
		controlBox.add(dragCheck);
		return controlBox;
	}

	/**
	 * Can we drag all highlighted items
	 */
	public JCheckBox createHighlightedDragableCheck() {
		final JCheckBox check = new JCheckBox("can drag highlighted items");
		check.setSelected(dragListeners.contains(highlightedDragListener));
		check.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (check.isSelected()) {
					addDragListener(highlightedDragListener);
				}
				else{
					removeDragListener(highlightedDragListener);
				}
			}
		});		
		return check;
	}
	/**
	 * can or cannot directly drag edges
	 */
	public JCheckBox createEdgeDragableCheck(){
		final JCheckBox check = new JCheckBox("can directly drag edges");
		check.setSelected(dragListeners.contains(edgeDragListener));
		check.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (check.isSelected()) {
					addDragListener(edgeDragListener);
				}
				else {
					removeDragListener(edgeDragListener);
				}
			}
		});		
		return check;
	}
	/**
	 * show items in high quality or not
	 */
	public JCheckBox createHighqualityCheck(){
		final JCheckBox check = new JCheckBox("show high quality");
		check.setSelected(isHighQuality());
		check.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				setHighQuality(check.isSelected());
				getVisualization().repaint();
			}
		});		
		return check;
	}
	
	/**
	 * Show grouped items in a tree structure
	 */
	public Component createGroupControlTree(FilterBox filter){
		if (!(getVisualization() instanceof MoreVisualization)) {
			Debug.prob("To show group tree, please use MoreVisualization");
			return new JPanel();
		}
		PreGroupModel pgm = new PreGroupModel(
				(MoreVisualization)m_vis, VisualItem.LABEL, VisualItem.VISIBLE);
		final PreJTreeTable treeTable = new PreJTreeTable(pgm){
			@Override
			public void updateUI() {
				super.updateUI();
				setUI(new PreGroupTreeTableUI());
			}
		};
		treeTable.getColumn(VisualItem.VISIBLE).setPreferredWidth(50);
		treeTable.getColumn(VisualItem.VISIBLE).setMaxWidth(75);
		treeTable.setFilter(filter);
		return treeTable.wrap(TREE_DIM);
	}
	
	private boolean itemTab = false;
	public void showItemControlTabInTree(){
		if (!(getVisualization() instanceof MoreVisualization)) {
			Debug.prob("To show item tab, please use MoreVisualization");
			return;
		}
		if (itemTab) {
			Debug.prob("You should only call this function once!");
			return;
		}
		itemTab = true;
		final MoreVisualization moreVis = (MoreVisualization) m_vis;
		
		final FilterBox filterBox = new FilterBox(moreVis);
		addWidget(ITEM_TAB, Box.createVerticalStrut(5));
		addWidget(ITEM_TAB, filterBox);
		Box filterInfoBox = Box.createHorizontalBox();
		final JCheckBox highLightCheck = new JCheckBox("highlight selected:");
		highLightCheck.setSelected(filterBox.enableHighlight());
		highLightCheck.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				filterBox.enableHighlight(highLightCheck.isSelected());
			}
		});
		filterInfoBox.add(highLightCheck);		
		JLabel filterLabel = new JLabel(" All items are selected");		
		filterLabel.setEnabled(false);
		filterInfoBox.add(filterLabel);
		filterBox.setInfoLabel(filterLabel);
		addWidget(ITEM_TAB, filterInfoBox);
		
		JLabel operationLabel = new JLabel(OperationBox.NO_OPERATION_INFO);
		operationLabel.setEnabled(false);
		OperationBox operationBox = new OperationBox(moreVis);
		operationBox.setFilter(filterBox);
		operationBox.addOperation(new GroupOperation(operationBox));
		operationBox.addOperation(new OpacityValueOperation(operationBox));
		operationBox.addOperation(new ColumnValueOperation(operationBox));
		operationBox.setInfoLabel(operationLabel);
		addWidget(ITEM_TAB, operationBox);
		addWidget(ITEM_TAB, operationLabel);
		addWidget(ITEM_TAB, Box.createVerticalStrut(5));
		//hack to make the treePanel to be in the center
		JPanel treePanel = (JPanel) createGroupControlTree(filterBox);
		addWidget(ITEM_TAB, treePanel);
		JPanel parent = (JPanel) treePanel.getParent();
		parent.add(treePanel, BorderLayout.CENTER);
		setControlTabsVisible(true);
	}
	
	/**
	 * Old version of {@link #showItemControlTabInTree()}, which use several
	 * tables to show the content of tuples. It also works, but you cannot see
	 * focus group information in this method. I still keep it here, but if the
	 * new version happens to have some bugs, you may try to call this instead.
	 */
	@Deprecated
	public void showItemControlTabInTables(){
		if (!(getVisualization() instanceof MoreVisualization)) {
			Debug.prob("To show item tab, please use MoreVisualization");
			return;
		}
		if (itemTab) {
			Debug.prob("You should only call this function once!");
			return;
		}
		itemTab = true;
		final MoreVisualization moreVis = (MoreVisualization) m_vis;
		final Map<String, PreJTable> map = new HashMap<String, PreJTable>();
		
		final FilterBox filterBox = new FilterBox(moreVis);
		addWidget(ITEM_TAB, filterBox);
		JLabel expressLabel = new JLabel(" All items are selected");
		expressLabel.setEnabled(false);
		filterBox.setInfoLabel(expressLabel);
		addWidget(ITEM_TAB, expressLabel);
		
		JLabel operationLabel = new JLabel(OperationBox.NO_OPERATION_INFO);
		operationLabel.setEnabled(false);
		OperationBox operationBox = new OperationBox(moreVis);
		operationBox.addOperation(new GroupOperation(operationBox));
		operationBox.addOperation(new OpacityValueOperation(operationBox));
		operationBox.addOperation(new ColumnValueOperation(operationBox));
		operationBox.setInfoLabel(operationLabel);
		addWidget(ITEM_TAB, operationBox);
		addWidget(ITEM_TAB, operationLabel);
		operationBox.setFilter(filterBox);
		GroupListener groupListener = new GroupListener() {
			public void groupUpdated(String[] added, String[] removed) {
				for (String group : removed) {
					PreJTable t = map.get(group);
					if (t == null){
						Debug.info("Ignore focus group:"+group);
						continue;
					}
					//basically, we have removed visual groups.
					//(neither focus groups nor composite groups)
					//we want to make them disappear from the display
					t.scrollPane().setVisible(false);
					t.scrollPane().getParent().remove(t.scrollPane());
					map.remove(group);
				}
				for (String group : added) {
					TupleSet ts = m_vis.getVisualGroup(group);
					if (ts == null || !(ts instanceof VisualTable)){
						continue;
					}
					VisualTable vt = (VisualTable) ts;
					String[] ns = null;
					Iterator iter = vt.tuples();
					if (iter.hasNext() && iter.next() instanceof EdgeItem){
						ns = new String[]{VISIBLE, STROKECOLOR, SIZE, STROKE};
					}
					if (ns == null) {
						ns = new String[]{
							X, Y, SIZE, STROKECOLOR, FILLCOLOR, VISIBLE, SHAPE
						};
					}
					ColumnProjection cp = new NamedColumnProjection(ns, true);
					PreJTable table = new PreJTable(vt, cp);
					JScrollPane pane = table.scrollView(group, TABLE_DIM);
					map.put(group, table);
					addWidget(ITEM_TAB, pane);
					table.setFilter(filterBox);
				}
			}
		};
		moreVis.addGroupListener(groupListener);
		
		//manually fire the update to create the jtable for the groups which
		//are already there
		String[] groups = moreVis.getGroups().toArray(new String[0]);
		groupListener.groupUpdated(groups, new String[0]);
		
		//show the contol panels
		setControlTabsVisible(true);
	}
	/**
	 * Call this function to show:
	 * 1) zoom / pan / drag control
	 * 2) save pdf/png buttons
	 * 3) group control table
	 * 4) high/low rendering quality switch
	 * 5) edge dragable switch
	 * 6) highlighted items drag switch
	 */
	public void showIODisplayTab(){
		if (ioDisplayTab) {
			Debug.prob("You should only call this function once!");
			return;
		}
		ioDisplayTab = true;
		//adding control control widgets
		addWidget(IO_DISPLAY_TAB, createDisplayControlBox());
		addWidget(IO_DISPLAY_TAB, createSaveImageBox());
		addWidget(IO_DISPLAY_TAB, createHighqualityCheck());
		addWidget(IO_DISPLAY_TAB, createEdgeDragableCheck());
		addWidget(IO_DISPLAY_TAB, createHighlightedDragableCheck());
		setControlTabsVisible(true);
	}
	
	private boolean ioDisplayTab = false;
	
	/**
	 * Show/hide the control pane on the right hand side.
	 * addWidget function will add component to this pane
	 */
	public void setControlTabsVisible(boolean visible){
		paneView();//make sure viewPane is not null;
		if (visible && (viewPane.getComponent(0) instanceof JPanel)){
			JSplitPane pane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
			pane.setResizeWeight(1);//we do not allow the size to update
			pane.add(this, JSplitPane.LEFT);
			pane.add(widgetTabs, JSplitPane.RIGHT);
			viewPane.add(pane, BorderLayout.CENTER);
		}
		if (!visible && (viewPane.getComponent(0) instanceof JSplitPane)){
			JPanel pane = new JPanel(new BorderLayout());
			pane.add(this, BorderLayout.CENTER);
			viewPane.add(pane, BorderLayout.CENTER);
		}
	}
	
	/**
	 * Wrap this display with a JPanel or JSplittedPane, 
	 * it depends on if the simpleControlPane is shown.
	 * 
	 * @return the JPanel/JSplittedPane containing this display
	 */
	public JComponent paneView(){
		if (viewPane == null) {
			viewPane = new JPanel(new BorderLayout());
			JPanel pane = new JPanel(new BorderLayout());
			pane.add(this, BorderLayout.CENTER);
			viewPane.add(pane, BorderLayout.CENTER);
		}
		return viewPane;
	}

	/**
	 * Get a JFrame view of the display. Usually after calling this functions,
	 * you should probably need to call {@link JFrame#pack()} and
	 * {@link JFrame#setVisible(boolean)} functions to finish the setup. 
	 * 
	 * @param title the name showing on the title bar
	 * @return a new JFrame component containing the paneView component only
	 */
	public JFrame frameView(String title){
		JFrame frame = new JFrame(title);
		frame.setContentPane(paneView());
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		return frame;
	}
	
	/**
	 * A convinence function of {@link #frameView(String)}.
	 */
	public JFrame showFrame(String title, boolean zoomToFit){
		JFrame frame = new JFrame(title);
		frame.setContentPane(paneView());
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.pack();
		frame.setVisible(true);
		if (zoomToFit) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					//zoom to fit
					Visualization vis = getVisualization();
			        Rectangle2D rect = vis.getBounds(Visualization.ALL_ITEMS);
			        GraphicsLib.expand(rect, 10 + (int)(1/getScale()));
			        DisplayLib.fitViewToBounds(BaseUIDisplay.this, rect, 1000);
				}
			});
		}
		return frame;
	}

	
	/**
	 * A convinence function of {@link #frameView(String)}.
	 */
	public JFrame showFrame(String title){
		JFrame frame = new JFrame(title);
		frame.setContentPane(paneView());
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.pack();
		frame.setVisible(true);
		return frame;
	}
}
